// Copyright (c) 2021 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package engine

import (
	"go/ast"
	"go/token"
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/uber-go/gopatch/internal/data"
)

func TestMetavar(t *testing.T) {
	tests := []struct {
		desc string

		// Name and type of the metavariable.
		mname string
		mtype MetavarType

		// Test cases for this matcher. Data generated by one case will be
		// carried over to the next.
		matches []matchCase

		// Value expected from the replacer given the data retrieved from
		// successful matches in matches.
		replace reflect.Value
	}{
		{
			desc:  "not a metavar",
			mname: "foo",
			matches: []matchCase{
				{
					desc: "ident match",
					give: refl(&ast.Ident{Name: "foo"}),
					ok:   true,
				},
				{
					desc: "ident mismatch",
					give: refl(&ast.Ident{Name: "bar"}),
				},
			},
			replace: refl(&ast.Ident{Name: "foo"}),
		},
		{
			desc:  "ident metavar",
			mname: "foo",
			mtype: IdentMetavarType,
			matches: []matchCase{
				{
					desc: "does not match expressions",
					give: refl(&ast.CallExpr{Fun: &ast.Ident{Name: "foo"}}), // == foo()
				},
				{
					desc: "matches any identifier",
					give: refl(&ast.Ident{Name: "bar"}),
					ok:   true,
				},
				{
					desc: "ignores other identifiers",
					give: refl(&ast.Ident{Name: "baz"}),
				},
				{
					desc: "ignores identifiers with the same name as the metavar",
					give: refl(&ast.Ident{Name: "foo"}),
				},
				{
					desc: "match captured identifier",
					give: refl(&ast.Ident{Name: "bar"}),
					ok:   true,
				},
			},
			replace: refl(&ast.Ident{Name: "bar"}),
		},
		{
			desc:  "expression metavar",
			mname: "foo",
			mtype: ExprMetavarType,
			matches: []matchCase{
				{
					desc: "does not match statements",
					give: refl(&ast.AssignStmt{
						Lhs: []ast.Expr{&ast.Ident{Name: "foo"}},
						Tok: token.DEFINE,
						Rhs: []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "42"}},
					}), // === foo := 42
				},
				{
					desc: "match any expression",
					give: refl(&ast.CallExpr{
						Fun: &ast.Ident{Name: "DoThing"},
						Args: []ast.Expr{
							&ast.BasicLit{Kind: token.INT, Value: "42"},
						},
					}), // == DoThing(42)
					ok: true,
				},
				{
					desc: "ignores other expressions",
					give: refl(&ast.Ident{Name: "foo"}),
				},
				{
					desc: "ignores similar expressions",
					give: refl(&ast.CallExpr{
						Fun: &ast.Ident{Name: "DoThing"},
						Args: []ast.Expr{
							&ast.BasicLit{Kind: token.INT, Value: "7"},
						},
					}), // == DoThing(7)
				},
				{
					desc: "match captured expression",
					give: refl(&ast.CallExpr{
						Fun: &ast.Ident{Name: "DoThing"},
						Args: []ast.Expr{
							&ast.BasicLit{Kind: token.INT, Value: "42"},
						},
					}), // == DoThing(42)
					ok: true,
				},
			},
			replace: refl(&ast.CallExpr{
				Fun: &ast.Ident{Name: "DoThing"},
				Args: []ast.Expr{
					&ast.BasicLit{Kind: token.INT, Value: "42"},
				},
			}), // == DoThing(42)
		},
	}

	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			meta := &Meta{
				Vars: make(map[string]MetavarType),
			}
			if tt.mtype != 0 {
				meta.Vars[tt.mname] = tt.mtype
			}

			fset := token.NewFileSet()
			ident := refl(&ast.Ident{Name: tt.mname})
			d := data.New()

			t.Run("Match", func(t *testing.T) {
				m := newMatcherCompiler(fset, meta, 0, 0).compileIdent(ident)
				if newd, ok := assertMatchCases(t, m, d, tt.matches); ok {
					d = newd
				}
			})

			t.Run("Replace", func(t *testing.T) {
				r := newReplacerCompiler(fset, meta, 0, 0).compileIdent(ident)

				got, err := r.Replace(d, NewChangelog(), 0)
				require.NoError(t, err)
				assert.Equal(t, tt.replace.Interface(), got.Interface())
			})
		})
	}
}

func TestMetavarErrors(t *testing.T) {
	t.Run("cannot produce a metavar that wasn't matched", func(t *testing.T) {
		fset := token.NewFileSet()
		foo := refl(&ast.Ident{Name: "foo"})
		bar := refl(&ast.Ident{Name: "bar"})

		meta := &Meta{
			Vars: map[string]MetavarType{
				"foo": IdentMetavarType,
				"bar": IdentMetavarType,
			},
		}

		d := data.New()

		// Match foo with x
		m := newMatcherCompiler(fset, meta, 0, 0).compileIdent(foo)
		d, ok := m.Match(refl(&ast.Ident{Name: "x"}), d, Region{})
		require.True(t, ok)

		// Attempt to replace bar.
		r := newReplacerCompiler(fset, meta, 0, 0).compileIdent(bar)
		_, err := r.Replace(d, NewChangelog(), 0)
		require.Error(t, err)
		assert.Contains(t, err.Error(), `could not find value for metavariable "bar"`)
	})
}
